/**
 * Licensed under the MIT License
 *
 * Copyright (c) 2010 Alexandre Croiseaux
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */
package marcel.display.gui.form
{
	import flash.display.DisplayObject;
	import flash.display.DisplayObjectContainer;
	import flash.display.InteractiveObject;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.FocusEvent;
	import flash.events.MouseEvent;
	import flash.geom.Rectangle;
	import marcel.core.Plugin;
	import marcel.debug.*;
	import marcel.display.gui.components.*;
	import marcel.utils.DateUtils;
	import swc.form.ErrorBadgeAsset;
	
	/**
	 * FormItem class used to register gui elements into an Form instance
	 * @author Alexandre Croiseaux
	 */
	public class FormItem
	{
		
		//--------------------------------------------------------------------------
		//
		//  Public vars
		//
		//--------------------------------------------------------------------------
		/**
		 * The form instance that is currenlty using the form item
		 */
		public var form:Form;
		/**
		 * The UI component associated mith this form item
		 */
		public var component:IFormComponent;
		/**
		 * The maximum characters allowed in the item
		 */
		public var maxChars:int = int.MAX_VALUE;
		/**
		 * The minimum characters allowed in the item
		 */
		public var minChars:int = 0;
		/**
		 * Indicates if the item is required, so the value must not be null
		 */
		public var required:Boolean;
		/**
		 * The minimum value allowed in the item, for numeric components
		 */
		public var minValue:Number;
		/**
		 * The maximum value allowed in the item, for numeric components
		 */
		public var maxValue:Number = Number.MAX_VALUE;
		/**
		 * The minimum age allowed in the item, for date components
		 */
		public var minAge:int;
		/**
		 * The minimum date for a date item
		 */
		public var minDate:String;
		/**
		 * The maximum date for a date item
		 */
		public var maxDate:String;
		/**
		 * The error label associated with the component to describe it. Displayed when an validation error occured
		 */
		public var errorLabel:String;
		/**
		 * The name of the current item, used to identify it
		 */
		public var name:String;
		/**
		 * An optional Regular Expression pattern that this item must match. For text items.
		 */
		public var regex:String;
		/**
		 * The default value displayed by the component if no user data is given
		 */
		public var defaultValue:*;
		/**
		 * The focus index order in the form
		 */
		public var tabIndex:uint;
		/**
		 * Indicates a item name against which the current value must match
		 */
		public var matchItem:String;
		/**
		 * Indicates the item associated with this item for clickable zones. For Label items.
		 */
		public var forItem:String;
		/**
		 * Indicates a custom error id to use when the validation of the current item is false.
		 */
		public var customErrorId:uint;
		
		
		//--------------------------------------------------------------------------
		//
		//  Private vars
		//
		//--------------------------------------------------------------------------
		private var _errorId:uint;
		private var _errorParams:Array;
		private var _badge:ErrorBadgeAsset;
		private var _badgeError:String;
		private var _useBadge:Boolean;
		private var _useHighlight:Boolean;
		private var _isTypingCheck:Boolean;
		
		
		//--------------------------------------------------------------------------
		//
		//  Constructor
		//
		//--------------------------------------------------------------------------
		/**
		 * Constructs a new FormItem instance
		 * @param	form		The Form instance this item will be added to
		 * @param	useHighlight Indicates if the form must automatically highlight componants on form errors by calling the IFormComponent.displayError method.
		 * @param	useBadge	Indicates if the form must automatically show a badge and a tooltip on form errors
		 */
		public function FormItem(form:Form, useHighlight:Boolean, useBadge:Boolean)
		{
			this.form = form;
			_badge = new ErrorBadgeAsset();
			_badge.x = -_badge.width - 2;
			_badge.addEventListener(MouseEvent.ROLL_OVER, onTooltip);
			_badge.addEventListener(MouseEvent.ROLL_OUT, onTooltip);
			_useBadge = useBadge;
			_useHighlight = useHighlight;
		}
		
		
		//--------------------------------------------------------------------------
		//
		//  Public vars
		//
		//--------------------------------------------------------------------------
		/**
		 * Parses and bulid the form item
		 * @param	xml	The xml data to build the form item from
		 */
		public function parseXML(xml:XML):void
		{
			switch (xml.@type.toString().toLowerCase())
			{
				case "label":
					var label:Label = new Label();
					label.setCSSStyle(getStyleClass(xml));
					fillData(xml, label);
					if (forItem && form.getItem(forItem) && form.getItem(forItem).component is CheckBox)
					{
						var cb:CheckBox = CheckBox(form.getItem(forItem).component);
						var rect:Rectangle = label.getBounds(label.parent);
						rect.x = label.x - cb.x;
						rect.y = label.y - cb.y;
						label.mouseEnabled = false;
						cb.setActiveZone(rect);
					}
					component = label;
					break;
				case "input":
					component = new InputText(getStyleClass(xml));
					fillData(xml, component);
					break;
				case "checkbox":
					if (xml.attribute("renderer").toString()) component = new CheckBox(null, null, null, xml.attribute("renderer").toString());
					else component = new CheckBox();
					(component as CheckBox).setStyle(getStyleClass(xml));
					fillData(xml, component);
					break;
				case "combobox":
					var renderer:String = xml.attribute("renderer").toString() ? xml.attribute("renderer").toString() : "marcel.display.gui.components.ListItemRenderer";
					var trackOnly:Boolean = xml.attribute("trackOnly").toString() == "true";
					component = new ComboBox(150, 5, false, xml.attribute("renderer").toString(), trackOnly);
					fillData(xml, component);
					break;
				case "list":
					var renderer2:String = xml.attribute("renderer").toString() ? xml.attribute("renderer").toString() : "marcel.display.gui.components.ListItemRenderer";
					var trackOnly2:Boolean = xml.attribute("trackOnly").toString() == "true";
					component = new List(150, 5, renderer, trackOnly);
					fillData(xml, component);
					break;
				case "radio":
					component = new RadioGroup();
					var items:Array = xml.@items.toString().split(",");
					for (var i:int = 0; i < items.length; i++)
					{
						if (form.getItem(items[i]) && form.getItem(items[i]).component is CheckBox) RadioGroup(component).addCheckBox(CheckBox(form.getItem(items[i]).component));
					}
					fillData(xml, component);
					break;
				case "hidden":
					component = null;
					fillData(xml, component);
					break;
			}
			
			// handle change
			if (component is InputText) component.addEventListener(FocusEvent.FOCUS_OUT, onChange);
			if (component is EventDispatcher) component.addEventListener(Event.CHANGE, onChange);
			
			
			// handle tabs
			if (component is InteractiveObject)
			{
				var o:InteractiveObject = component as InteractiveObject;
				o.tabEnabled = false;
				if (o is DisplayObjectContainer)
				{
					DisplayObjectContainer(o).tabChildren = (o is InputText);
					if (tabIndex > 0) DisplayObjectContainer(o).tabIndex = tabIndex;
				}
			}
		}
		
		/**
		 * Returns the current value of the item
		 */
		public function getValue():*
		{
			if (component == null) return defaultValue;
			return component.getValue();
		}
		
		/**
		 * Sets the specified value to the current item and component
		 */
		public function setValue(value:*):void
		{
			if (component != null) component.setValue(value);
			else defaultValue = value;
		}
		
		/**
		 * Indicates if the current item is valid and respects all conditions
		 * @return	true if the form is valid (no error found), false otherwise
		 */
		public function isValid():Boolean
		{
			if (component == null) return true;
			
			var valid:Boolean = validateValue(component.getValue());
			if (valid)
			{
				if (_useHighlight && component)
				{
					component.displayError(false);
				}
				
				if (_useBadge)
				{
					if ((component is DisplayObjectContainer) && DisplayObjectContainer(component).contains(_badge))
					{
						DisplayObjectContainer(component).removeChild(_badge);
					}
					else if ((component is RadioGroup) && RadioGroup(component).getFirstCB().contains(_badge))
					{
						RadioGroup(component).getFirstCB().removeChild(_badge);
					}
				}
			}
			return valid;
		}
		
		/**
		 * Returns the localized error message of the current error
		 */
		public function getErrorMessage():String
		{
			return _badgeError;
		}
		
		/**
		 * set the current Item as invalid with the specified error. This method is automatically called during the form validation, bust can be called if needed.
		 * @param	errorId	The error id specified in the errors xml
		 * @param	params	The params to replace in the error message
		 * @return	always false
		 */
		public function setError(errorId:uint, params:Array):Boolean
		{
			if (errorId == 4 && ((component is List) || (component is ComboBox))) errorId = 11;
			_errorId = errorId;
			_errorParams = params;
			_badgeError = form.getErrorText(_errorId, _errorParams);
			if (!_isTypingCheck)
			{
				if (_useHighlight && component)
				{
					component.displayError(true);
				}
				
				if (_useBadge && component)
				{
					if ((component is DisplayObjectContainer) && !DisplayObjectContainer(component).contains(_badge))
					{
						DisplayObjectContainer(component).addChild(_badge);
					}
					else if ((component is RadioGroup) && !RadioGroup(component).getFirstCB().contains(_badge))
					{
						RadioGroup(component).getFirstCB().addChild(_badge);
					}
				}
			}
			return false;
		}
		
		
		
		//--------------------------------------------------------------------------
		//
		//  Private vars
		//
		//--------------------------------------------------------------------------
		private function validateValue(value:*):Boolean
		{
			// default/null/matching values
			if (required && value == null) return error(4, [errorLabel]);
			if (required && value == defaultValue) return error(4, [errorLabel]);
			if (matchItem && form.getItem(matchItem) && value != form.getItem(matchItem).getValue()) return error(9, [errorLabel, form.getItem(matchItem).errorLabel]);
			
			// String tests
			if (value is String)
			{
				if ((value == defaultValue || value.length == 0) && !required) return true;
				
				if (required && value.length == 0) return error(4, [errorLabel]);
				if (value.length < minChars) return error(5, [errorLabel, minChars]);
				if (value.length > maxChars) return error(6, [errorLabel, maxChars]);
				if (regex)
				{
					var reg:RegExp = new RegExp(regex, "i");
					if (!reg.test(value)) return error(7, [errorLabel]);
				}
				if (minAge)
				{
					try
					{
						var ok:Boolean = DateUtils.getAge(DateUtils.getDateFromString(value)) >= minAge;
						if (!ok) return error(12, [minAge]);
					}
					catch (e:Error) { return error(12, [minAge]); }
				}
				if (minDate || maxDate)
				{
					var t:uint = DateUtils.getDateFromString(value).getTime();
					if (minDate && (t < DateUtils.getDateFromString(minDate).getTime())) return error(13, [errorLabel, minDate]);
					if (maxDate && (t > DateUtils.getDateFromString(maxDate).getTime())) return error(14, [errorLabel, maxDate]);
				}
			}
			
			// Number tests
			if (!isNaN(Number(value)))
			{
				if (value < minValue || value > maxValue) return error(8, [errorLabel, minValue, maxValue]);
			}
			
			// Bool tests
			if (value is Boolean)
			{
				if (required && !value) return error(10, [errorLabel]);
			}
			
			// OK
			return true;
		}
		
		private function error(errorId:uint, params:Array):Boolean
		{
			if (customErrorId > 0) return setError(customErrorId, params);
			return setError(errorId, params);
		}
		
		private function cast(value:*):*
		{
			if (value == "true" || value == "false") return value == "true";
			if (!isNaN(value) && value != "") return Number(value);
			return value;
		}
		
		private function fillData(xml:XML, component:IFormComponent):void
		{
			// attributes
			for each (var attr:XML in xml.attributes())
			{
				setPropValue(attr.name().toString(), attr.toString(), component);
			}
			
			// sub nodes
			for each (var node:XML in xml.children())
			{
				setPropValue(node.name().toString(), node.toString(), component);
			}
		}
		
		private function setPropValue(attrName:String, val:String, component:IFormComponent):void
		{
			var value:* = cast(Plugin.plugin ? Plugin.plugin.config.parseValue(val) : val);
			if (component) try	{ component[attrName] = value; } catch (e:Error) { /*trace(e);*/ }
			try	{ this[attrName] = value; } catch (e:Error) { /*trace(e);*/ }
			
			if (attrName == "badgeXOffset") _badge.x += value;
			if (attrName == "badgeYOffset") _badge.y += value;
		}
		
		private function getStyleClass(xml:XML):String
		{
			if (xml["class"].toString()) return xml["class"].toString();
			return xml.attribute("class");
		}
		
		
		//--------------------------------------------------------------------------
		//
		//  Events
		//
		//--------------------------------------------------------------------------
		private function onTooltip(e:MouseEvent):void
		{
			if (e.type == MouseEvent.ROLL_OVER) form.openToolTip(e.target as DisplayObject, _badgeError);
			else form.closeToolTip();
		}
		
		private function onChange(e:Event):void
		{
			_isTypingCheck = (component is InputText) && e.type != FocusEvent.FOCUS_OUT; // si le user tape dans le champ, on ne doit afficher l'erreur qu'au focus out
			isValid();
			_isTypingCheck = false;
		}
	}

}